package com.javabi.htmlbuilder.html;

import java.util.ArrayList;
import java.util.List;

import com.javabi.htmlbuilder.DocumentBuilder;
import com.javabi.htmlbuilder.css.CSSBuilder;
import com.javabi.htmlbuilder.css.CSSDeclarationSet;

/**
 * An HTML Builder.
 */
public class HTMLBuilder extends DocumentBuilder<HTMLBuilder> {

	/** The indent. */
	private final String indent;
	/** The element name list. */
	private final List<HTMLElement<?>> elementList = new ArrayList<HTMLElement<?>>();
	/** Indicates if format. */
	private boolean formatted = false;
	/** The status */
	private Status status = Status.ROOT;

	/**
	 * Creates a new HTML builder.
	 * @param indent the indentation whitespace.
	 */
	public HTMLBuilder(String indent) {
		this.indent = indent;
	}

	/**
	 * Creates a new HTML builder.
	 */
	public HTMLBuilder() {
		this("\t");
	}

	@Override
	public String toString() {
		while (!isRoot()) {
			close();
		}
		return document.toString();
	}

	/**
	 * Append an indent.
	 */
	protected void indent() {
		if (indent != null) {
			final int count = elementList.size();
			for (int i = 0; i < count; i++) {
				document.append(indent);
			}
		}
	}

	/**
	 * Returns true to format the top element.
	 * @return true to format the top element.
	 */
	private boolean isFormatted() {
		HTMLElement<?> element = top();
		return element != null && element.format();
	}

	/**
	 * Format (new line & indent)!
	 */
	private void format() {
		if (formatted) {
			newLine();
			indent();
		}
	}

	/**
	 * Pushes the given element.
	 * @param element the element to add.
	 */
	private void push(HTMLElement<?> element) {
		if (element == null) {
			throw new NullPointerException();
		}
		elementList.add(element);
	}

	/**
	 * Pops the top element.
	 * @return the element removed.
	 */
	private HTMLElement<?> pop() {
		return elementList.remove(elementList.size() - 1);
	}

	/**
	 * Returns the top element.
	 * @return the top element.
	 */
	private HTMLElement<?> top() {
		if (elementList.isEmpty()) {
			return null;
		}
		return elementList.get(elementList.size() - 1);
	}

	/**
	 * Returns the true if this is the root of the document.
	 * @return the true if this is the root of the document.
	 */
	private boolean isRoot() {
		return elementList.isEmpty();
	}

	/**
	 * Open a new HTML element.
	 * @param element the element.
	 * @param attributes true to leave the element open for attributes.
	 * @return this builder.
	 */
	public HTMLBuilder open(HTMLElement<?> element, boolean attributes) {
		if (status == Status.ATTRIBUTES) {
			document.append('>');
		}
		format();
		push(element);
		document.append('<');
		document.append(element.getElementName());
		if (attributes) {
			status = Status.ATTRIBUTES;
		} else {
			document.append('>');
			status = Status.CONTENT;
		}
		formatted = element.format();
		return this;
	}

	/**
	 * Close the current HTML element.
	 * @param element the expected element.
	 * @return this builder.
	 */
	public HTMLBuilder close(HTMLElement<?> element) {
		if (status == Status.ROOT) {
			throw new IllegalStateException("no open HTML element to close");
		}
		HTMLElement<?> popped = pop();
		if (element != null && popped != element) {
			throw new IllegalStateException("expected <" + element.getElementName() + ">, found <" + popped.getElementName() + ">");
		}
		if (status == Status.ATTRIBUTES) {
			document.append('/');
		} else {
			format();
			document.append('<');
			document.append('/');
			document.append(popped.getElementName());
		}
		document.append('>');
		formatted = isFormatted();
		if (isRoot()) {
			status = Status.ROOT;
		} else {
			status = Status.CONTENT;
		}
		return this;
	}

	/**
	 * Close the current HTML element.
	 * @return this builder.
	 */
	public HTMLBuilder close() {
		return close(null);
	}

	/**
	 * Append an empty element.
	 * @param element the element.
	 * @return this builder.
	 */
	public HTMLBuilder empty(HTMLElement<?> element) {
		return element(element, null);
	}

	/**
	 * Append an element element.
	 * @param element the element.
	 * @param value the element value.
	 * @return this builder.
	 */
	public HTMLBuilder element(HTMLElement<?> element, Object value) {
		if (status == Status.ATTRIBUTES) {
			document.append('>');
		}
		format();
		document.append('<');
		document.append(element.getElementName());
		if (value == null) {
			document.append('/');
		} else {
			document.append('>');
			formatted = element.format();
			format();
			document.append(value);
			format();
			document.append('<');
			document.append('/');
			document.append(element.getElementName());
		}
		document.append('>');
		if (isRoot()) {
			status = Status.ROOT;
		} else {
			status = Status.CONTENT;
		}
		formatted = isFormatted();
		return this;
	}

	/**
	 * Append an attribute.
	 * @param name the attribute name.
	 * @param value the attribute value.
	 * @return this builder.
	 */
	public HTMLBuilder attribute(String name, Object value) {
		if (name == null || value == null) {
			throw new NullPointerException();
		}
		if (status != Status.ATTRIBUTES) {
			throw new IllegalStateException("no open HTML element");
		}
		document.append(' ');
		document.append(name);
		document.append('=');
		document.append('\"');
		document.append(value);
		document.append('\"');
		return this;
	}

	/**
	 * Append an attribute.
	 * @param name the attribute name.
	 * @param value the attribute value.
	 * @return this builder.
	 */
	public HTMLBuilder attribute(String name, int value) {
		if (name == null) {
			throw new NullPointerException();
		}
		if (status != Status.ATTRIBUTES) {
			throw new IllegalStateException("no open HTML element");
		}
		document.append(' ');
		document.append(name);
		document.append('=');
		document.append('\"');
		document.append(value);
		document.append('\"');
		return this;
	}

	/**
	 * Indicate content is about to commence.
	 */
	public void content() {
		if (status != Status.CONTENT) {
			if (status == Status.ROOT) {
				throw new IllegalStateException("no open HTML element to close");
			}
			if (status == Status.ATTRIBUTES) {
				document.append('>');
				status = Status.CONTENT;
				format();
			}
		}
	}

	/**
	 * Appends the given content.
	 * @param text the content.
	 */
	public void content(String text) {
		content();
		document.append(text);
	}

	/**
	 * Appends the given content.
	 * @param number the content.
	 */
	public void content(int number) {
		content();
		document.append(number);
	}

	/**
	 * Appends a named entity.
	 * @param name the name.
	 * @return this builder.
	 */
	public HTMLBuilder entity(String name) {
		content();
		document.append('&');
		document.append(name);
		document.append(';');
		return this;
	}

	/**
	 * Append a CSS style.
	 * @param style the style.
	 * @return this builder.
	 */
	public HTMLBuilder css(CSSDeclarationSet style) {
		document.append(style);
		return this;
	}

	/**
	 * Append a CSS builder.
	 * @param css the css builder.
	 * @return this builder.
	 */
	public HTMLBuilder css(CSSBuilder css) {
		document.append(css);
		return this;
	}

	/**
	 * Appends a numeric entity.
	 * @param number the number.
	 * @return this builder.
	 */
	public HTMLBuilder entity(int number) {
		content();
		document.append('&');
		document.append(number);
		document.append(';');
		return this;
	}

	/**
	 * The Status.
	 */
	private static enum Status {
		ROOT, ATTRIBUTES, CONTENT;
	}

}
